class Token {
  tokenType: number;
  literal: string;
  lineNumber: number;
  constructor(type: number, literal: string, lineNumber: number) {
    this.tokenType = type;
    this.literal = literal;
    this.lineNumber = lineNumber;
  }

  type = () => {
    return this.tokenType;
  };

  getLiteral = () => {
    return this.literal;
  };

  getLineNumber = () => {
    return this.lineNumber;
  };
}

class MonkeyLexer {
  sourceCode: any;
  position: number;
  readPosition: number;
  lineCount: number;
  ch: string | number;
  ILLEAGAL!: number;
  EOF!: number;
  LET!: number;
  IDENTIFIER!: number;
  EQUAL_SIGN!: number;
  PLUS_SIGN!: number;
  INTEGER!: number;
  SEMICOLON!: number;
  keyWordMap!: { [key in string]: any };
  IF!: number;
  ELSE!: number;
  observer: any;
  observerContext: any;
  constructor(sourceCode: any) {
    this.initTokenType();
    this.initKeywords();
    this.sourceCode = sourceCode;
    this.position = 0;
    this.readPosition = 0;
    this.lineCount = 0;
    this.ch = '';
  }

  setLexingObserver = (o, context) => {
    if (o !== null && o !== undefined) {
      this.observer = o;
      this.observerContext = context;
    }
  };

  notifyObserver = (token) => {
    this.observer.notifyTokenCreation(
      token,
      this.observerContext,
      this.position - 1,
      this.readPosition,
    );
  };

  getKeywords = () => {
    return this.keyWordMap;
  };

  initKeywords = () => {
    this.keyWordMap = [];
    this.keyWordMap['let'] = new Token(this.LET, 'let', 0);
    this.keyWordMap['if'] = new Token(this.IF, 'if', 0);
    this.keyWordMap['else'] = new Token(this.ELSE, 'else', 0);
  };

  initTokenType = () => {
    this.ILLEAGAL = -2;
    this.EOF = -1;
    this.LET = 0;
    this.IDENTIFIER = 1;
    this.EQUAL_SIGN = 2;
    this.PLUS_SIGN = 3;
    this.INTEGER = 4;
    this.SEMICOLON = 5;

    this.IF = 6;
    this.ELSE = 7;
  };

  readChar = () => {
    if (this.readPosition >= this.sourceCode.length) {
      this.ch = 'LITERAL_END';
    } else {
      this.ch = this.sourceCode[this.readPosition];
    }

    this.position = this.readPosition;
    this.readPosition++;
  };

  skipWhiteSpaceAndNewLine = () => {
    while (this.ch === ' ' || this.ch === '\t' || this.ch === '\n') {
      if (this.ch === '\t' || this.ch === '\n') {
        this.lineCount++;
      }

      this.readChar();
    }
  };

  nextToken = () => {
    let tok;
    this.skipWhiteSpaceAndNewLine();
    let lineCount = this.lineCount;
    let needReadChar = true;
    this.position = this.readPosition;

    switch (this.ch) {
      case '=':
        tok = new Token(this.EQUAL_SIGN, '=', lineCount);
        break;
      case ';':
        tok = new Token(this.SEMICOLON, ';', lineCount);
        break;
      case '+':
        tok = new Token(this.EQUAL_SIGN, '+', lineCount);
        break;
      case 'LITERAL_END':
        tok = new Token(this.EOF, '', lineCount);
        break;
      default:
        let res = this.readIdentifier();
        if (res !== false) {
          if (this.keyWordMap[res] !== undefined) {
            tok = this.keyWordMap[res];
          } else {
            tok = new Token(this.IDENTIFIER, res, lineCount);
          }
        } else {
          res = this.readNumber();
          if (res !== false) {
            tok = new Token(this.INTEGER, res, lineCount);
          }
        }

        if (res === false) {
          tok = undefined;
        }
    }

    if (needReadChar === true) {
      this.readChar();
    }

    if (tok !== undefined) {
      this.notifyObserver(tok);
    }

    return tok;
  };

  isLetter = (ch: string | number) => {
    return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || ch === '_';
  };

  isDigit = (ch: string | number) => {
    return '0' <= ch && ch <= '9';
  };

  readIdentifier = () => {
    let identifier = '';
    while (this.isLetter(this.ch)) {
      identifier += this.ch;
      this.readChar();
    }

    if (identifier.length > 0) {
      return identifier;
    } else {
      return false;
    }
  };

  readNumber = () => {
    let number = '';

    while (this.isDigit(this.ch)) {
      number += this.ch;
      this.readChar();
    }

    if (number.length > 0) {
      return number;
    } else {
      return false;
    }
  };

  lexing = () => {
    this.readChar();

    const tokens = [];
    let token = this.nextToken();

    while (token?.type() !== this.EOF) {
      tokens.push(token);
      token = this.nextToken();
    }

    console.log(tokens, 'render tokens fff ddd');
  };
}

export { MonkeyLexer, Token };
